<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h3 data-lake-id="gCFH5" id="gCFH5"><span data-lake-id="u8def5f5d" id="u8def5f5d">问题发现</span></h3>
  <p data-lake-id="u75e99f8c" id="u75e99f8c"><br></p>
  <p data-lake-id="u045b71da" id="u045b71da"><span data-lake-id="u7d5c6801" id="u7d5c6801">在一个内部平台中，系统提供了一个功能，允许管理员导出报表数据为Excel文件。随着平台用户量的增长和数据量的积累，管理员开始报告导出操作时系统频繁崩溃。</span></p>
  <p data-lake-id="uc0aaa77e" id="uc0aaa77e"><span data-lake-id="u20fe6697" id="u20fe6697">​</span><br></p>
  <p data-lake-id="ufd9e4340" id="ufd9e4340"><span data-lake-id="ua2662378" id="ua2662378">监控系统显示，在执行导出操作期间，应用服务器的内存使用量急剧上升，最终导致Java虚拟机抛出 OutOfMemoryError 异常。</span></p>
  <p data-lake-id="ud817c399" id="ud817c399"><br></p>
  <h3 data-lake-id="HuoDb" id="HuoDb"><span data-lake-id="u2b3e96ae" id="u2b3e96ae">问题排查</span></h3>
  <p data-lake-id="u6140b3a8" id="u6140b3a8"><br></p>
  <p data-lake-id="u7f2c4875" id="u7f2c4875"><span data-lake-id="u9328404f" id="u9328404f">首先，我们需要确认是哪部分代码或对象导致了内存溢出。这可以通过分析堆转储（Heap Dump）来实现。当 OutOfMemoryError 发生时，可以配置JVM自动生成堆Dump文件，或者使用诸如 jmap 工具手动生成堆Dump。</span></p>
  <p data-lake-id="u48c66b0d" id="u48c66b0d"><span data-lake-id="u55c1296b" id="u55c1296b">​</span><br></p>
  <pre lang="java"><code>
-XX:+HeapDumpOnOutOfMemoryError

配置OOM时自动生成Dump文件
</code></pre>
  <p data-lake-id="ueb5e54fb" id="ueb5e54fb"><span data-lake-id="u6aa92b4b" id="u6aa92b4b" class="lake-fontsize-12" style="color: var(--text-primary)">​</span><br></p>
  <pre lang="java"><code>
jmap -dump:live,format=b,file=heapdump.hprof &lt;pid&gt;
</code></pre>
  <p data-lake-id="u61bf6b2b" id="u61bf6b2b"><br></p>
  <p data-lake-id="u9a2087e8" id="u9a2087e8"><span data-lake-id="u1499f250" id="u1499f250">这里面建议在GC前和GC后分别获取Dump，如果只获取一次，一定要知道是GC前的还是GC后的。不然很容易分析错误。</span></p>
  <p data-lake-id="u081a0b85" id="u081a0b85"><br></p>
  <p data-lake-id="u4c0e33ce" id="u4c0e33ce"><span data-lake-id="u6414749c" id="u6414749c">接下来使用堆分析工具MAT加载和分析 heapdump.hprof 文件。通过分析，我们发现存在大量的 org.apache.poi.xssf.usermodel.XSSFWorkbook 实例占用了大部分堆内存。</span></p>
  <p data-lake-id="ue8b9342b" id="ue8b9342b"><br></p>
  <p data-lake-id="ufd972885" id="ufd972885"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1700378903070-68af4a3c-9227-4f46-8fc4-383639dfcd6a.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_47%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u070e2889" id="u070e2889" style="text-align: center"><em><span data-lake-id="u9074b091" id="u9074b091">（当时的分析截图未保存，这个截图是我从网上找的，几乎和我们当时现象一模一样）</span></em></p>
  <p data-lake-id="u0e6fa1cc" id="u0e6fa1cc"><br></p>
  <p data-lake-id="u55f3b2dd" id="u55f3b2dd"><span data-lake-id="u6a4328d8" id="u6a4328d8">定位到使用 Apache POI 的代码部分，发现系统在处理大量数据时，是将所有数据一次性加载到 XSSFWorkbook 对象中的。</span></p>
  <p data-lake-id="u22939025" id="u22939025"><span data-lake-id="u27e07209" id="u27e07209">​</span><br></p>
  <p data-lake-id="u4f1d40f8" id="u4f1d40f8"><span data-lake-id="u4d925504" id="u4d925504">我们的业务代码中确实有使用XSSF的部分代码如下：</span></p>
  <p data-lake-id="u3b81efe2" id="u3b81efe2"><span data-lake-id="u809973c6" id="u809973c6">​</span><br></p>
  <pre lang="java"><code>
public ByteArrayOutputStream exportData(List&lt;Data&gt; dataList) {
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet = workbook.createSheet("Data");
  
    int rownum = 0;
    for (Data data : dataList) {
        Row row = sheet.createRow(rownum++);
        // ... 填充数据行 ...
    }
  
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    workbook.write(out);
    return out;
}

</code></pre>
  <p data-lake-id="u24977030" id="u24977030"><br></p>
  <h3 data-lake-id="yuo6O" id="yuo6O"><span data-lake-id="u094b86fe" id="u094b86fe">问题解决</span></h3>
  <p data-lake-id="u8c3700ce" id="u8c3700ce"><br></p>
  <p data-lake-id="ub056439a" id="ub056439a"><span data-lake-id="u39f124cc" id="u39f124cc">问题定位到了，解决起来就简单了。根据下文：</span></p>
  <p data-lake-id="ue9e9430a" id="ue9e9430a"><span data-lake-id="u93dedab4" id="u93dedab4">​</span><br></p>
  <p data-lake-id="uee3864ba" id="uee3864ba"><br></p>
  <p data-lake-id="u1197910f" id="u1197910f"><br></p>
  <p data-lake-id="uea6936f5" id="uea6936f5"><span data-lake-id="u5cc51b71" id="u5cc51b71">我们知道，可以使用SXSSF来解决这个问题，因为SXSSFWorkbook 这种方式将数据写入临时文件，而不是保留在内存中。</span></p>
  <p data-lake-id="u3d799c30" id="u3d799c30"><span data-lake-id="u1ef5d1ef" id="u1ef5d1ef">​</span><br></p>
  <pre lang="java"><code>
public ByteArrayOutputStream exportData(List&lt;Data&gt; dataList) {
		// 使用SXSSFWorkbook，并保留100行数据在内存中，其余写入磁盘
    SXSSFWorkbook workbook = new SXSSFWorkbook(100); 
    Sheet sheet = workbook.createSheet("Data");

    int rownum = 0;
    for (Data data : dataList) {
        Row row = sheet.createRow(rownum++);
        // ... 填充数据行 ...
    }

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    workbook.write(out);
    out.close();

    // 清除临时文件
    workbook.dispose();
    return out;
}

</code></pre>
  <p data-lake-id="u16b3cefc" id="u16b3cefc"><br></p>
  <h3 data-lake-id="Gckws" id="Gckws"><span data-lake-id="uce706fe5" id="uce706fe5">后续改进</span></h3>
  <p data-lake-id="ucd1e70b7" id="ucd1e70b7"><br></p>
  <p data-lake-id="u0ff45104" id="u0ff45104"><span data-lake-id="u04eb848e" id="u04eb848e">通过优化后，解决了内存溢出的问题，但是随着文件越来越大，会存在导出实践过长的问题，可以考虑用异步在后台生成报表的方式。这里就不展开了。</span></p>
 </body>
</html>